package be.kuleuven.cs.mume;

import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;

import org.jbox2d.collision.AABB;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.World;
import org.mt4j.MTApplication;
import org.mt4j.components.MTComponent;
import org.mt4j.components.TransformSpace;
import org.mt4j.components.interfaces.IMTComponent3D;
import org.mt4j.components.visibleComponents.shapes.AbstractShape;
import org.mt4j.components.visibleComponents.shapes.MTLine;
import org.mt4j.input.gestureAction.DefaultPanAction;
import org.mt4j.input.gestureAction.TapAndHoldVisualizer;
import org.mt4j.input.inputProcessors.IGestureEventListener;
import org.mt4j.input.inputProcessors.MTGestureEvent;
import org.mt4j.input.inputProcessors.componentProcessors.lassoProcessor.LassoProcessor;
import org.mt4j.input.inputProcessors.componentProcessors.panProcessor.PanProcessorTwoFingers;
import org.mt4j.input.inputProcessors.componentProcessors.tapAndHoldProcessor.TapAndHoldEvent;
import org.mt4j.input.inputProcessors.componentProcessors.tapAndHoldProcessor.TapAndHoldProcessor;
import org.mt4j.input.inputProcessors.componentProcessors.tapProcessor.TapProcessor;
import org.mt4j.input.inputSources.KeyboardInputSource;
import org.mt4j.sceneManagement.AbstractScene;
import org.mt4j.util.MTColor;
import org.mt4j.util.math.Vector3D;
import org.mt4j.util.math.Vertex;

import advanced.physics.physicsShapes.PhysicsRectangle;
import advanced.physics.util.PhysicsHelper;
import advanced.physics.util.UpdatePhysicsAction;
import be.kuleuven.cs.mume.actions.UpdateConnLine;
import be.kuleuven.cs.mume.gestures.MyListener;
import be.kuleuven.cs.mume.objects.MediaObject;
import be.kuleuven.cs.mume.objects.PhysicsRoundedRectangle;
import be.kuleuven.cs.mume.objects.MediaObject.MediaType;

public class WorldController {
	private static WorldController singletonWorld;
	AbstractScene mscene;
	MTApplication m_mtApplication;
	Data data;
	World world;
	List<PhysicsRoundedRectangle> phyXs = new ArrayList<PhysicsRoundedRectangle>();
	private float scale = 1;
	private float timeStep = 1.0f / (2.5f * this.scale);
	private int constraintIterations = 100;
	private MTComponent physicsContainer;

	private List<PhysicsRectangle> borders;
//	private LassoProcessor lasso;

	// private LassoProcessor lasso;

	private WorldController() {
		// Needed for singleton
	}

	public WorldController(MTApplication mtApplication, AbstractScene scene) {
		MediaObject artist;
		this.mscene = scene;
		this.m_mtApplication = mtApplication;
		createPhysicsWorld();

		float x = this.m_mtApplication.getWidth() / 2;
		float y = this.m_mtApplication.getHeight() / 2;
		artist = new MediaObject(this.m_mtApplication, x, y, "Soulwax",
				MediaType.ARTIST);
		this.data = new Data();
		artist.registerInputProcessor(new TapProcessor(this.m_mtApplication));
		artist.addGestureListener(TapProcessor.class, new MyListener());
		artist.setPickable(false);

		this.mscene.getCanvas().registerInputProcessor(
				new PanProcessorTwoFingers(mtApplication));
		this.mscene.getCanvas().addGestureListener(
				PanProcessorTwoFingers.class, new DefaultPanAction());

		borders = new ArrayList<PhysicsRectangle>(4);
		// lasso = new LassoProcessor(m_mtApplication, mscene.getCanvas(),
		// mscene.getSceneCam());
		// mscene.getCanvas().registerInputProcessor(lasso);
		// mscene.getCanvas().addGestureListener(LassoProcessor.class, new
		// DefaultLassoAction(m_mtApplication,
		// mscene.getCanvas().getClusterManager(), mscene.getCanvas()));

		float w = artist.getWidthXY(TransformSpace.GLOBAL);
		float h = artist.getHeightXY(TransformSpace.GLOBAL);
		AbstractShape artistPhys = createPhysicsObject(w, h, new Vector3D(x, y));
		artistPhys.addChild(artist);

		artist.setPositionGlobal(artistPhys.getCenterPointGlobal());
		this.physicsContainer.addChild(artistPhys);
		singletonWorld = this;
		GravListener grav = new GravListener(mtApplication, world, phyXs,
				physicsContainer);
	}

	public void objectClicked(IMTComponent3D item) {
		MTComponent itemComponent = (MTComponent) item;
		MediaObject o = (MediaObject) itemComponent.getChildByIndex(0);
		if (MediaType.ARTIST == o.getType())
			addForArtistNode(o);
		else
			addForSongNode(o);
	}

	public void addForArtistNode(MediaObject o) {
		System.out.println("Artist node, find songs by " + o.getNodeName());
		ArrayList<Song> songs = null;
		songs = this.data.getAllSongsByArtist(o.getNodeName(), o.conSongs);
		addSongNodes(o, songs, songs.size());
	}

	public void addForSongNode(MediaObject o) {
		System.out.println("Song node, find remixes with id " + o.getId()
				+ " and the artists");
		ArrayList<Song> songs = null;
		ArrayList<String> artists = null;
		songs = this.data.getAllSongsBySong(o.getId(), o.conSongs);
		artists = this.data.getAllArtistsBySong(o.getId(), o.conArtists);
		addArtistNodes(o, artists, artists.size() + songs.size());
		addSongNodes(o, songs, artists.size() + songs.size());
	}

	public void addArtistNodes(MediaObject o, ArrayList<String> artists,
			int size) {
		float x = this.m_mtApplication.getWidth() / 2 + 100;
		float y = this.m_mtApplication.getHeight() / 2 - 100;
		for (int i = 0; i < artists.size(); i++) {
			addArtistNode(o, artists.get(i), size, i);
		}
	}

	public void addSongNodes(MediaObject o, ArrayList<Song> songs, int size) {
		int done = size - songs.size();
		for (int i = 0; i < songs.size(); i++) {
			addSongNode(o, songs.get(i), size, done + i);
		}
	}

	public void addArtistNode(MediaObject parent, String artistName, int size,
			int index) {
		float x = parent.getCenterPointGlobal().x;
		float y = parent.getCenterPointGlobal().y;
		MediaObject artist = new MediaObject(this.m_mtApplication, x, y,
				artistName, MediaType.ARTIST);
		artist.setId(null);
		parent.addArtistConnection(artistName);
		addConnectionFromParent(parent, artist);
		finaliseNode(parent, artist, size, index);
	}

	public void addSongNode(MediaObject parent, Song songData, int size,
			int index) {
		float x = parent.getCenterPointGlobal().x;
		float y = parent.getCenterPointGlobal().y;
		MediaObject song = new MediaObject(this.m_mtApplication, x, y,
				songData.title, MediaType.SONG);
		song.setId(songData.id);
		parent.addSongConnection(songData.id);
		addConnectionFromParent(parent, song);
		finaliseNode(parent, song, size, index);
	}

	public void addConnectionFromParent(MediaObject parent, MediaObject child) {
		if (parent.getType() == MediaType.ARTIST)
			child.addArtistConnection(parent.getNodeName());
		else
			child.addSongConnection(parent.getId());
	}

	public void finaliseNode(MediaObject parent, MediaObject child, int size,
			int i) {
		child.registerInputProcessor(new TapProcessor(this.m_mtApplication));
		child.addGestureListener(TapProcessor.class, new MyListener());
		MTLine connectLine = connectLine(parent, child);

		Vector3D childPos = parent.getCenterPointGlobal();
		float parW = parent.getWidthXY(TransformSpace.GLOBAL);
		float parH = parent.getHeightXY(TransformSpace.GLOBAL);
		float newX = (float) Math.sin(2 * Math.PI / size * i) * parW;
		float newY = (float) Math.cos(2 * Math.PI / size * i) * parH;
//		Vector3D newPos = new Vector3D(newX, newY);
//		for (AbstractShape comp : this.phyXs) {
//			Vector3D compC = comp.getCenterPointGlobal();
//			Vector3D parC = comp.getCenterPointGlobal();
//			float parDist = newPos.distance2D(parC);
//			float obsDist = newPos.distance2D(compC);
//			float parObsDist = parC.distance2D(compC);
//			float relX = parC.x - newX;
//			float relY = parC.y - newY;
//			float z = parDist / (obsDist + parObsDist);
//			if (z > 0.80) {
//				relX /= 2;
//				relY /= 2;
//				parDist = newPos.distance2D(parC);
//				Vector3D newPos2=new Vector3D(newPos.getX()- relX,newPos.getY()-relY);
//				parDist = newPos2.distance2D(parC);
//				obsDist = newPos2.distance2D(compC);
//				parObsDist = parC.distance2D(compC);
//				z = parDist / (obsDist + parObsDist);
//				if (z > 0.8)
//					continue;
//				else{
//					newPos=newPos2;
//				}
//			}
//			
//		}

		float width = child.getWidthXY(TransformSpace.GLOBAL);
		float height = child.getHeightXY(TransformSpace.GLOBAL);
		childPos.translate(new Vector3D(newX, newY));
		PhysicsRoundedRectangle rect = createPhysicsObject(width, height, childPos);

		// mscene.getCanvas().addChild(connectLine);
		rect.addChild(child);
		this.physicsContainer.addChild(rect);
		this.physicsContainer.addChild(connectLine);
//		this.mscene.registerPreDrawAction(new UpdateConnLine(parent, child,
//				connectLine, physicsContainer, this.mscene));
		Vector3D childPosRel = child.getCenterPointRelativeToParent();
		System.out.println(childPosRel);
		Vec2 impulse = new Vec2((childPos.x - parent.getCenterPointGlobal().x) * 1000 * size,(childPos.y - parent.getCenterPointGlobal().y)*1000 * size);
		rect.getBody().applyImpulse(impulse, rect.getBody().getWorldCenter());

		child.setPickable(false);
		child.setPositionGlobal(rect.getCenterPointGlobal());
	}

	private MTLine connectLine(AbstractShape parent, AbstractShape child) {
		Vertex startPoint = new Vertex(parent.getCenterPointGlobal());
		Vertex endPoint = new Vertex(child.getCenterPointGlobal());
		startPoint.z = -1;
		endPoint.x = -1;
		MTLine connectLine = new MTLine(this.m_mtApplication, startPoint,
				endPoint);
		connectLine.setStrokeWeight(3f);
		connectLine.setStrokeColor(MTColor.WHITE);
		connectLine.setPickable(false);
		UpdateConnLine update = new UpdateConnLine(parent, child, connectLine,
				physicsContainer);
		this.mscene.registerPreDrawAction(update);
		this.mscene.getCanvas().addChild(connectLine);
		return connectLine;
	}

	private PhysicsRoundedRectangle createPhysicsObject(float width, float height,
			Vector3D pos) {
		// if(pos.x < 0) {
		// pos.x = width / 2;
		// } else if (pos.x > this.m_mtApplication.width) {
		// pos.x = this.m_mtApplication.width - width / 2;
		// }
		// if(pos.y < 0) {
		// pos.y = height / 2;
		// } else if (pos.y > this.m_mtApplication.height) {
		// pos.y = this.m_mtApplication.height - height / 2;
		// }

		PhysicsRoundedRectangle rect = new PhysicsRoundedRectangle(width,
				height, 0f, 1, pos, this.m_mtApplication, this.world, /**/250/width/*/1/**/, 0f,
				.8f, this.scale);
		PhysicsHelper.addDragJoint(this.world, rect,
				rect.getBody().isDynamic(), this.scale);

		rect.registerInputProcessor(new TapAndHoldProcessor(m_mtApplication,
				2000));
		rect.addGestureListener(TapAndHoldProcessor.class,
				new TapAndHoldVisualizer(m_mtApplication, this.mscene
						.getCanvas()));
		rect.addGestureListener(TapAndHoldProcessor.class,
				new IGestureEventListener() {
					public boolean processGestureEvent(MTGestureEvent ge) {
						TapAndHoldEvent th = (TapAndHoldEvent) ge;
						switch (th.getId()) {
						case TapAndHoldEvent.GESTURE_DETECTED:
							break;
						case TapAndHoldEvent.GESTURE_UPDATED:
							break;
						case TapAndHoldEvent.GESTURE_ENDED:
							if (th.isHoldComplete()) {
								PhysicsRoundedRectangle r = ((PhysicsRoundedRectangle) ge
										.getTargetComponent());
								if (!r.isRemoved()) {
									r.reCreateBody(false, true);
									System.out.println("Remove");
								} else {
									r.reCreateBody(false, false);
									System.out.println("Reset");
								}
							}
							break;
						default:
							break;
						}
						return false;
					}
				});
		// this.lasso.addClusterable(rect);
		rect.setNoFill(true);
		rect.setNoStroke(true);
		rect.registerInputProcessor(new TapProcessor(this.m_mtApplication));
		rect.addGestureListener(TapProcessor.class, new MyListener());
		this.phyXs.add(rect);
		return rect;
	}

	private void createPhysicsWorld() {
		float worldOffset = 100000; // Make Physics world slightly bigger than
		// screen borders
		// Physics world dimensions
		Vec2 lowerVertex = new Vec2(-worldOffset, -worldOffset);
		float upperx = (this.m_mtApplication.width) / this.scale + worldOffset;
		float uppery = (this.m_mtApplication.height) / this.scale + worldOffset;
		Vec2 upperVertex = new Vec2(upperx, uppery);
		AABB worldAABB = new AABB(lowerVertex, upperVertex);
		Vec2 gravity = new Vec2(0, 0);
		boolean sleep = true;
		// Create the physics world
		this.world = new World(worldAABB, gravity, sleep);
		UpdatePhysicsAction update = new UpdatePhysicsAction(this.world,
				this.timeStep, this.constraintIterations, this.scale);
		this.mscene.registerPreDrawAction(update);
		this.physicsContainer = new MTComponent(this.m_mtApplication);
		// Scale the physics container. Physics calculations work best when the
		// dimensions are small (about 0.1 - 10 units)
		// So we make the display of the container bigger and add in turn make
		// our physics object smaller
		this.physicsContainer.scale(this.scale, this.scale, 1,
				Vector3D.ZERO_VECTOR);
		this.mscene.getCanvas().addChild(this.physicsContainer);
		// createScreenBorders(this.physicsContainer);
	}

	private void createScreenBorders(MTComponent parent) {
		if(borders.size() != 0){
			removeBorders(parent);
		}
		float x = mscene.getSceneCam().getPosition().x - m_mtApplication.width
				/ 2;
		float y = mscene.getSceneCam().getPosition().y - m_mtApplication.height
				/ 2;
		System.out.println(x + " " + y);
		// Left border
		float borderWidth = 50f;
		float borderHeight = this.m_mtApplication.height;
		Vector3D pos = new Vector3D(-(borderWidth / 2f) + x,
				(this.m_mtApplication.height / 2f) + y);
		PhysicsRectangle borderLeft = new PhysicsRectangle(pos, borderWidth,
				borderHeight, this.m_mtApplication, this.world, 0, 0, 0,
				this.scale);
		borderLeft.setName("borderLeft");
		borders.add(borderLeft);
		parent.addChild(borderLeft);
		// Right border
		pos = new Vector3D(this.m_mtApplication.width + (borderWidth / 2) + x,
				this.m_mtApplication.height / 2 + y);
		PhysicsRectangle borderRight = new PhysicsRectangle(pos, borderWidth,
				borderHeight, this.m_mtApplication, this.world, 0, 0, 0,
				this.scale);
		borderRight.setName("borderRight");
		borders.add(borderRight);
		parent.addChild(borderRight);
		// Top border
		borderWidth = this.m_mtApplication.width;
		borderHeight = 50f;
		pos = new Vector3D(this.m_mtApplication.width / 2 + x,
				-(borderHeight / 2) + y);
		PhysicsRectangle borderTop = new PhysicsRectangle(pos, borderWidth,
				borderHeight, this.m_mtApplication, this.world, 0, 0, 0,
				this.scale);
		borderTop.setName("borderTop");
		borders.add(borderTop);
		parent.addChild(borderTop);
		// Bottom border
		pos = new Vector3D(this.m_mtApplication.width / 2 + x,
				this.m_mtApplication.height + (borderHeight / 2) + y);
		PhysicsRectangle borderBottom = new PhysicsRectangle(pos, borderWidth,
				borderHeight, this.m_mtApplication, this.world, 0, 0, 0,
				this.scale);
		borderBottom.setName("borderBottom");
		borders.add(borderBottom);
		parent.addChild(borderBottom);
	}
	
	private void removeBorders(MTComponent parent){
		if(borders.size()==0)
			return;
		for(PhysicsRectangle r : borders){
			physicsContainer.removeChild(r);
			world.destroyBody(r.getBody());
		}
		System.out.println("clearBorder");
		borders.clear();
	}

	public static WorldController getSingleton() {
		if (singletonWorld == null) {
			synchronized (WorldController.class) {
				if (singletonWorld == null) {
					singletonWorld = new WorldController();
				}
			}
		}
		return singletonWorld;
	}

	public class GravListener extends KeyboardInputSource {

		private World world;
		private int gToggle = 0;
		private int bToggle = 0;
		private List<PhysicsRoundedRectangle> phyXs;
		private MTComponent physicsConatiner;

		public GravListener(MTApplication pa, World world,
				List<PhysicsRoundedRectangle> phyXs,
				MTComponent physicsContainer) {
			super(pa);
			this.world = world;
			this.phyXs = phyXs;
			this.physicsConatiner = physicsContainer;
		}

		@Override
		public void keyEvent(KeyEvent e) {
			switch (e.getKeyCode()) {
			// Toggle properties
			case KeyEvent.VK_G:
				if (this.gToggle==1) {
					createScreenBorders(this.physicsConatiner);
					this.world.setGravity(new Vec2(0, 1f));
					
					for (PhysicsRoundedRectangle r : this.phyXs) {
						r.reCreateBody(true,true);
						((AbstractShape) r.getChildByIndex(0)).setPositionGlobal(r.getCenterPointGlobal());
						System.out.println("reCreate");
					}
				}else if(this.gToggle == 3){
					removeBorders(this.physicsConatiner);
					this.world.setGravity(new Vec2());
					
					for (PhysicsRoundedRectangle r : this.phyXs) {
						r.reCreateBody(false,false);
						((AbstractShape) r.getChildByIndex(0)).setPositionGlobal(r.getCenterPointGlobal());
						System.out.println("reCreate");
					}
				}
				this.gToggle = (++gToggle)%4;
				break;
			case KeyEvent.VK_B:
				if (this.bToggle==1) {
					createScreenBorders(this.physicsConatiner);
				}else if(this.bToggle == 3){
					removeBorders(this.physicsConatiner);
				}
				this.bToggle = (++bToggle)%4;
				break;
			}
		}

	}

}
